﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using JigLibX.Physics;
using JigLibX.Collision;
using JigLibX.Geometry;
using JigLibX.Math;
using JigLibX.Utils;
using Microsoft.Xna.Framework;

namespace DARE
{
    /// <summary>
    /// All methods that return a list of intersected objects are already ordered by distance.
    /// TODO:
    /// - IntersectOrderFromNodeTo...
    /// BUG: 
    /// - only the heightmap collide through the character, wtf ?
    /// - no active camera ? segfault ! :D
    /// </summary>
    public class CRayCast
    {

    #region identifier

        public class CIntersectInfo
        {
            public CNode node;
            public Vector3 pos;
            public Vector3 normal;

            public CIntersectInfo()
            {

            }

            public CIntersectInfo(CNode _node, Vector3 _pos, Vector3 _normal)
            {
                node = _node;
                pos = _pos;
                normal = _normal;
            }
        }

    #endregion

    #region fields

        private CRender3D m_render;
        private float m_incStep;

    #endregion

    #region properties

        /// <summary>
        /// This value is used when an object is hitted several times.
        /// It's added to the current hit position to advance the ray.
        /// A small value will make the ray hit a lot of times in the same object, but a 
        /// too big value can skip little object from collisions...
        /// </summary>
        public float IncStep
        {
            get { return m_incStep; }
            set { if (value > 0) m_incStep = value; }
        }

    #endregion

    #region ctor

        public CRayCast(CRender3D render)
        {
            Initialize(render);
        }

        private void Initialize(CRender3D render)
        {
            m_render = render;
            m_incStep = 0.2f;
        }

    #endregion

    #region userfriendly methods
        
        public List<CIntersectInfo> IntersectAllFromCenterToMouse(int depth, int maxObject)
        {
            Vector2 mousePos = CDare.Instance.InputMgr.Mouse.Position;
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            return IntersectAll(m_render.CameraMgr.ActiveCamera.Position, 
                RayFromCenterTo(mousePos.X, mousePos.Y) * depth, 
                pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectAllFromCenterToNode(CNode node, int depth, int maxObject)
        {
            Vector3 camPos = m_render.CameraMgr.ActiveCamera.Position;
            Vector3 ray = node.Position - camPos;
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            ray.Normalize();
            return IntersectAll(camPos, ray, pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectAllFromCenterTo(Vector3 position, int depth, int maxObject)
        {
            Vector3 camPos = m_render.CameraMgr.ActiveCamera.Position;
            Vector3 ray = position - camPos;
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            ray.Normalize();
            return IntersectAll(camPos, ray, pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectAllFromNodeTo(CNode node, Vector3 direction, int depth, int maxObject)
        {
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            direction.Normalize();
            return IntersectAll(node.Position, direction, pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectAllFromPositionTo(Vector3 position, Vector3 direction, int depth, int maxObject)
        {
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            direction.Normalize();
            return IntersectAll(position, direction, pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectOrderFromCenterToMouse(int depth, int maxObject)
        {
            Vector2 mousePos = CDare.Instance.InputMgr.Mouse.Position;
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();
            Vector3 ray = RayFromCenterTo(mousePos.X, mousePos.Y);

            // =======================================================================================================
            //CDare.Instance.DebugMgr.DebugConsole.WriteLine("class mousepos: " + mousePos.ToString());
            //CDare.Instance.DebugMgr.DebugConsole.WriteLine("class ray: " + ray.ToString());
            //CDare.Instance.DebugMgr.DebugConsole.WriteLine("class cam: " + m_render.CameraMgr.ActiveCamera.Position.ToString());
            // =======================================================================================================

            return IntersectOrder(m_render.CameraMgr.ActiveCamera.Position, 
                RayFromCenterTo(mousePos.X, mousePos.Y), 
                pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectOrderFromCenterToNode(CNode node, int depth, int maxObject)
        {
            Vector3 camPos = m_render.CameraMgr.ActiveCamera.Position;
            Vector3 ray = node.Position - camPos;
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            ray.Normalize();
            return IntersectOrder(camPos, ray, pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectOrderFromCenterTo(Vector3 position, int depth, int maxObject)
        {
            Vector3 camPos = m_render.CameraMgr.ActiveCamera.Position;
            Vector3 ray = position - camPos;
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            ray.Normalize();
            return IntersectOrder(camPos, ray, pred, depth, maxObject);
        }

        public List<CIntersectInfo> IntersectOrderFromNodeTo(CNode node, Vector3 direction, int depth, int maxObject)
        {
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            direction.Normalize();
            return IntersectOrder(node.Position, direction, pred, depth, maxObject);
        }

        /// <summary>
        /// Direction have to be normalized.
        /// </summary>
        /// <param name="position"></param>
        /// <param name="direction"></param>
        /// <param name="depth"></param>
        /// <param name="maxObect"></param>
        /// <returns></returns>
        public List<CIntersectInfo> IntersectOrderFromPositionTo(Vector3 position, Vector3 direction, int depth, int maxObect)
        {
            CollisionSkinPredicate1 pred = new CCollisionPredicateAll();

            direction.Normalize();
            return IntersectOrder(position, direction, pred, depth, maxObect);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="position"></param>
        /// <param name="direction"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        public CIntersectInfo IntersectFirst(Vector3 position, Vector3 direction, int depth)
        {
            CIntersectInfo info0 = IntersectFirstObject(position, direction);
            Vector3 terPos = Vector3.Zero;
            Vector3 terNormal = Vector3.Zero;
            Vector3 terTmp = Vector3.Zero;
            CTerrainInfo terInfo = null;

            foreach (CTerrainInfo terrainInfo in m_render.TerrainInfos)
                if ((terTmp = IntersectTerrain(terrainInfo.OwnerNode, position, direction, out terNormal, depth)) != Vector3.Zero)
                {
                    terPos = terTmp;
                    terInfo = terrainInfo;
                    break;
                }
            if (terPos == Vector3.Zero)
                return info0;
            CIntersectInfo info1 = new CIntersectInfo(terInfo.OwnerNode, terPos, terNormal);
            return (info0 == null) ? info1 : ((terPos - position).LengthSquared() < (info0.pos - position).LengthSquared()) ? info1 : info0;
        }

    #endregion

    #region methods

        public Vector3 RayFromCenterTo(float x, float y)
        {
            Vector3 nearSource = new Vector3(x, y, 0);
            Vector3 farSource = new Vector3(x, y, 1);
            Matrix world = Matrix.CreateTranslation(0, 0, 0);
            CCamera camera = m_render.CameraMgr.ActiveCamera;
            Vector3 nearPoint = CDare.Instance.GraphicsDevice.Viewport.Unproject(nearSource, camera.Projection, camera.View, world);
            Vector3 farPoint = CDare.Instance.GraphicsDevice.Viewport.Unproject(farSource, camera.Projection, camera.View, world);
            Vector3 direction = farPoint - nearPoint;
            direction.Normalize();

            return direction;
        }
         
        public Vector3 IntersectTerrain(CollisionSkin skin, Vector3 origin, Vector3 ray, out Vector3 normal, int depth)
        {
            Vector3 pos = Vector3.Zero;
            float frac;
            
            ray.Normalize();
            normal = Vector3.Zero;
            for (int i = 0; i < depth; ++i)
            {
                if (skin.SegmentIntersect(out frac, out pos, out normal, new Segment(origin, ray)))
                    return pos;
                origin += ray;
            }
            return Vector3.Zero;
        }

        public Vector3 IntersectTerrain(CNode terrainNode, Vector3 origin, Vector3 ray, out Vector3 normal, int depth)
        {
            return IntersectTerrain(terrainNode.Entity.Physic.Skin, origin, ray, out normal, depth);
        }

        public List<CIntersectInfo> IntersectAll(Vector3 origin, Vector3 ray, CollisionSkinPredicate1 pred, int depth, int maxObject)
        {
            List<CIntersectInfo> listObj = IntersectObject(origin, ray * depth, pred, maxObject);
            CTerrainInfo terrainInfo = null;
            Vector3 objDistance = Vector3.Zero;
            float terDistance = 0.0f;
            Vector3 terPos = Vector3.Zero;
            Vector3 terNormal = Vector3.Zero;
            int index = 0;

            foreach (CTerrainInfo tInfo in m_render.TerrainInfos)
            {
                terrainInfo = tInfo;
                if (terPos != Vector3.Zero)
                    break;
                if (terrainInfo.OwnerNode != null)
                    terPos = IntersectTerrain(terrainInfo.OwnerNode, origin, ray, out terNormal, depth);
            }
            if (terPos == Vector3.Zero)
                return listObj;
            terDistance = (terPos - origin).LengthSquared();
            foreach (CIntersectInfo hInfo in listObj)
            {
                objDistance = hInfo.pos - origin;
                if (terDistance < objDistance.LengthSquared())
                    break;
                index += 1;
            }
            listObj.Insert(index, new CIntersectInfo(terrainInfo.OwnerNode, terPos, terNormal));
            return listObj;
        }

        // optimize this, dont call intersectall, and do all inside this method
        public List<CIntersectInfo> IntersectOrder(Vector3 origin, Vector3 ray, CollisionSkinPredicate1 pred, int depth, int maxObject)
        {
            List<CIntersectInfo> listObj = IntersectAll(origin, ray, pred, depth, maxObject);
            int index = 0;
            CIntersectInfo iInfo;
            CNode node;
            bool deleteGround = false;
            bool deleteClickable = false;

            if (listObj.Count > 0)
                iInfo = listObj.First();
            while (index < listObj.Count)
            {
                node = listObj.ElementAt(index).node;
                switch (node.Entity.Physic.CollisionType)
                {
                    case APhysicObject.ECollisionType.GROUND:
                        if (deleteGround == true)
                        {
                            listObj.RemoveAt(index);
                            index -= 1;
                        }
                        else
                            deleteGround = true;
                        break;

                    case APhysicObject.ECollisionType.CLICKABLE:
                        if (deleteClickable == true)
                        {
                            listObj.RemoveAt(index);
                            index -= 1;
                        }
                        else
                            deleteClickable = true;
                        break;

                    default: break;
                }
                index += 1;
            }
            return listObj;
        }

        public List<CIntersectInfo> IntersectObject(Vector3 origin, Vector3 ray, CollisionSkinPredicate1 pred, int maxObject)
        {
            List<CIntersectInfo> listObj = new List<CIntersectInfo>();
            CIntersectInfo intInfo = null;
            CNode previousNode = null;
            Vector3 rayNormalized = ray;

            rayNormalized.Normalize();
            for (int j = 0; (j < maxObject) && ((intInfo = IntersectFirstObject(origin, ray, pred)) != null);)
            {
                if (!intInfo.node.Equals(previousNode) && (intInfo.node.Entity.Physic.CollisionType != APhysicObject.ECollisionType.NONE))
                    listObj.Add(intInfo);

                // we hit the same object than the last time
                if (intInfo.node.Equals(previousNode))
                {
                    origin = intInfo.pos + rayNormalized * m_incStep;
                    continue;
                }

                previousNode = intInfo.node;
                origin = intInfo.pos;
                j += 1;
            }
            return listObj;
        }

        public CIntersectInfo IntersectFirstObject(Vector3 origin, Vector3 ray)
        {
            return IntersectFirstObject(origin, ray, new CCollisionPredicateAll());
        }

        // return the object intersected even if it's flagged at NONE (low level method)
        public CIntersectInfo IntersectFirstObject(Vector3 origin, Vector3 ray, CollisionSkinPredicate1 pred)
        {
            float frac;
            CollisionSkin skin;
            Vector3 pos;
            Vector3 normal;
            CNode node;

            if (m_render.PhysicsSystem.CollisionSystem.SegmentIntersect(
                out frac, out skin, out pos, out normal, 
                new Segment(origin, ray), pred))
            {
                if (m_render.CollisionNode.ContainsKey(skin))
                {
                    node = m_render.CollisionNode[skin];
                    //if (node.Entity.Physic.CollisionType != APhysicObject.ECollisionType.NONE)
                    return new CIntersectInfo(m_render.CollisionNode[skin], pos, normal);
                }
            }
            return null;
        }

    #endregion

    }
}
